﻿// <copyright file="Loader.cs" company="Oleg V. Polikarpotchkin">
// Copyright © 2009 Oleg V. Polikarpotchkin. All Right Reserved
// </copyright>
// <author>Oleg V. Polikarpotchkin</author>
// <email>ov-p@yandex.ru</email>
// <date>2009-04-12</date>
// <summary>Class to load DataPointSeries collection from the Generic Data Points Series XML file or Stream.</summary>
// <revision>$Id$</revision>

using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace OpenWPFChart.Helpers.XmlItemDataReader
{
	/// <summary>
	/// Loads <see cref="DataPointSeries"/> collection from XML file or Stream.
	/// </summary>
	/// <remarks>
	/// Contents of <see cref="DataPointSeries"/> XML is validated against a XSD schema. 
	/// That schema is dynamically constructed from the contents of XML mapping data.
	/// </remarks>
	public static class Loader
	{
		// XML namespace must be used in XML data files.
		internal const string dataNamespaceName = "urn:ItemData-schema";

		/// <summary>
		/// Loads <see cref="DataPointSeries"/> collection from the 
		/// <see cref="System.IO.TextReader"/> specified.
		/// </summary>
		/// <param name="dataReader">Input XML data <see cref="System.IO.TextReader"/>.</param>
		/// <param name="mappingReader">Input XML mapping <see cref="System.IO.TextReader"/>.</param>
		/// <returns>
		/// <see cref="DataPointSeries"/> collection on success; null on error.
		/// </returns>
		public static IEnumerable<DataPointSeries> Load(TextReader dataReader, TextReader mappingReader)
		{
			// Load mappings.
			List<Mapping> mappings = Mapping.Load(mappingReader);

			// Prepaire XmlReaderSettings for input file validation.
			XmlReaderSettings settings = new XmlReaderSettings();
			settings.ValidationType = ValidationType.Schema;
			settings.Schemas.Add(SchemaBuilder.Build(mappings));
			StringBuilder sbErrors = null;
			List<ValidationException.ValidationError> errors = null;
			settings.ValidationEventHandler += (sender, e) =>
			{
				if (sbErrors == null)
					sbErrors = new StringBuilder();
				sbErrors.AppendFormat("Validation error: {1}{0}Line={2}, position={3}{0}"
					, System.Environment.NewLine, e.Exception.Message
					, e.Exception.LineNumber, e.Exception.LinePosition);
				if (errors == null)
					errors = new List<ValidationException.ValidationError>();
				errors.Add(new ValidationException.ValidationError()
				{
					Message = e.Exception.Message,
					Line = e.Exception.LineNumber,
					Position = e.Exception.LinePosition
				});
			};

			// Load and validate the file.
			using (XmlReader reader = XmlReader.Create(dataReader, settings))
			{
				XElement items = XElement.Load(reader);
				if (sbErrors != null)
					// Validation error(s) occured.
					throw new ValidationException(sbErrors.ToString(), errors.ToArray());

				XNamespace xns = dataNamespaceName;
				// Check the root element name (i.e. Items in "urn:PointSeries-schema" xmlns).
				if (items.Name != xns + "Items")
					throw new ValidationException(string.Format("Root element {0} missed", xns + "Items"));
				// Parse the Point.XXX elements.
				return items.Elements().Select<XElement, DataPointSeries>(
					(item) =>
					{
						// Parse item tag name for (X/Y/Z) type strings.
						string[] types = getTypeStrings(item.Name.ToString());

						// Dot-separated type string.
						StringBuilder sb = new StringBuilder();
						foreach (string type in types)
						{
							if (sb.Length == 0)
								sb.Append(type);
							else
								sb.AppendFormat(".{0}", type);
						}
						//string xyzType = xType == yType ? xType : xType + "." + yType;
						Mapping map = (from mapItem in mappings
									   where mapItem.DotSeparatedTypeString == sb.ToString()
									   select mapItem).Single();

						// Optional attributes.
						var xName = item.Attribute("XName");
						var yName = item.Attribute("YName");
						var zName = item.Attribute("ZName");
						var itemType = item.Attribute("ItemType");

						Dimension[] dimensions = new Dimension[types.Length <= 2 ? 2 : types.Length];
						for (int i = 0; i < dimensions.Length; i++)
						{
							Dimension dim = new Dimension();
							switch (i)
							{
								case 0:
									dim.Name = xName == null ? "" : xName.Value;
									break;
								case 1:
									dim.Name = yName == null ? "" : yName.Value;
									break;
								case 2:
									dim.Name = zName == null ? "" : zName.Value;
									break;
							}
							dim.TypeString = map.Axes[i].TypeString;
							dim.XsdTypeString = map.Axes[i].XsdTypeString;
							dim.ClrType = map.Axes[i].ClrType;
							dimensions[i] = dim;
						}
						DataPointSeries series = new DataPointSeries()
						{
							Dimensions = dimensions,
							ItemType = itemType == null ? null : itemType.Value
						};
						foreach (var pt in from pt in item.Elements(xns + "Point") select pt)
						{
							XAttribute xAttr = pt.Attribute("x");
							XAttribute yAttr = pt.Attribute("y");
							XAttribute zAttr = pt.Attribute("z");
							string[] xsdPoints = new string[dimensions.Length];
							for (int i = 0; i < xsdPoints.Length; i++)
							{
								switch (i)
								{
									case 0:
										xsdPoints[i] = (string)xAttr;
										break;
									case 1:
										xsdPoints[i] = (string)yAttr;
										break;
									case 2:
										xsdPoints[i] = (string)zAttr;
										break;
								}
							}
							series.XsdPoints.Add(xsdPoints);
						}
						return series;
					});
			}
		}
		/// <summary>
		/// Loads <see cref="DataPointSeries"/> collection from the 
		/// <see cref="System.IO.Stream"/> specified.
		/// </summary>
		/// <param name="dataStream">Input XML data <see cref="System.IO.Stream"/>.</param>
		/// <param name="mappingStream">Input XML mapping <see cref="System.IO.Stream"/>.</param>
		/// <returns><see cref="DataPointSeries"/> collection on success; null on error.</returns>
		public static IEnumerable<DataPointSeries> Load(Stream dataStream, Stream mappingStream)
		{
			IEnumerable<DataPointSeries> series = null;
			using (TextReader rdrMapping = new StreamReader(mappingStream))
			using (TextReader rdrData = new StreamReader(dataStream))
			{
				series = Load(rdrData, rdrMapping);
			}
			return series;
		}
		/// <summary>
		/// Loads <see cref="DataPointSeries"/> collection from the file specified.
		/// </summary>
		/// <param name="dataFileName">Name of the data file.</param>
		/// <param name="mappingFileName">Name of the mapping file.</param>
		/// <returns><see cref="DataPointSeries"/> collection on success; null on error.</returns>
		public static IEnumerable<DataPointSeries> Load(string dataFileName, string mappingFileName)
		{
			IEnumerable<DataPointSeries> series = null;
			using (Stream stmMapping = File.OpenRead(mappingFileName))
			using (Stream stmData = File.OpenRead(dataFileName))
			{
				series = Load(stmData, stmMapping);
			}
			return series;
		}

		/// <summary>
		/// Parses the point series element tag and returns x,y,z,... type strings.
		/// </summary>
		/// <param name="tagName">Tag name.</param>
		static string[] getTypeStrings(string tagName)
		{
			int n = tagName.IndexOf('}');
			Debug.Assert(n > 0, "n > 0");
			const string pointsTagPrefix = "Points";
			int pointsTagPrefixLength = pointsTagPrefix.Length;
			Debug.Assert(tagName.Length > n + pointsTagPrefixLength + 1, "tagName.Length > n + pointsTagPrefixLength + 1");

			// Composite type string like Int or Int.Int or Int.Int.Int
			string types = tagName.Substring(n + pointsTagPrefixLength + 2);
			return types.Split(new char[] { '.' });
		}
	}
}
